跳到主要内容

Java JVM学习-JVM基本概念

Jvm 的主要组成部分?及其作用?

  • 类加载器(ClassLoader)
  • 运行时数据区(Runtime Data Area)
  • 执行引擎(Execution Engine)
  • 本地库接口(Native Interface)

各组件的作用:首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

整个执行流程

虚拟机是什么

所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为 系统虚拟机程序虚拟机

  • Visual Box、Mware 就属于系统虚拟机,它们完全是 对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。
  • 程序虚拟机的典型代表就是 Java 虚拟机,它专门为执行单个计算机程序而设计,在 Java 虚拟机中执行的指令称为 Java 字节码指令。

无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。

JRE、JDK和JVM的关系

JRE(Java Runtime Environment, Java运行环境) 是 Java 平台,所有的程序都要在 JRE 下才能够运行。包括 JVM 和 Java 核心类库和支持文件。

JDK(Java Development Kit,Java开发工具包) 是用来编译、调试 Java 程序的开发工具包。包括 Java 工具(javac/java/jdb等)和 Java 基础的类库(java API )。JDK 的工具也是 Java 程序,也需要 JRE 才能运行。为了保持 JDK 的独立性和完整性,在 JDK 的安装过程中,JRE 也是安装的一部分。所以,在 JDK 的安装目录下有一个名为 jre 的目录,用于存放 JRE 文件。

JVM(Java Virtual Machine, Java虚拟机) 是 JRE 的一部分。JVM 主要工作是解释自己的指令集(即字节码)并映射到本地的 CPU 指令集和 OS 的系统调用。Java 语言是跨平台运行的,不同的操作系统会有不同的 JVM 映射规则,使之与操作系统无关,完成跨平台性。JVM 有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。

JVM 是什么

JVM 是 Java Virtual Machine(Java虚拟机)的缩写,JVM 是一种用于计算设备的规范,它是一个 虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java 虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM 屏蔽了与具体操作系统平台相关的信息,使 Java 程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM 在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

Java 语言的一个非常重要的特点就是与平台的无关性。而使用 Java 虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入 Java 语言虚拟机后,Java 语言在不同平台上运行时不需要重新编译。Java 语言使用 Java 虚拟机屏蔽了与具体平台相关的信息,使得 Java 语言编译程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是 Java 的能够 “一次编译,到处运行” 的原因。

使用 JVM 跨平台的语言

JVM 作为一个可以将 Class 文件转换成机器码的平台,只要安装了虚拟机,就可以在上面运行字节码。同样,只要其他原因在编译过程中生成了字节码(所以不同的语言只要提供自己的编译器,将其转成字节码文件),那么照样可以通过 JVM 在不同平台上运行,这就实现了跨平台能力了。

JVM 根本不关心运行在其内部的程序到底是使用何种编程语言编写的,它只关心 “字节码” 文件。也就是说 Java虚拟机拥有语言无关性,并不会单纯地与 Java语言绑定,只要其它编程语言的编译结果满足并包含 JVM 的内部指令集、符号表以及其他的辅助信息,它就是一个有效的字节码文件,就能够被虚拟机所识别并装载运行

目前主流的基于 JVM 的脚本语言

  • Kotlin:Kotlin 可以编译成 Java 字节码,也可以编译成 JavaScript。(Kotlin 是 Android官方开发语言)
  • Groovy:基于 JVM 平台的动态编程语言
  • Scala
  • Jython:用 Java 语言写的 Python 解析器,可以无缝与 Java 类结合
  • JRuby:用来桥接 Java 与 Ruby
  • Fantom
  • Clojure
  • Rhino

使用字节码的好处

在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

JVM 的执行引擎

参考资料 JVM基础系列第4讲:从源代码到机器码,发生了什么?

执行引擎的位置:

执行引擎包含三部分:解释器,及时编译器,垃圾回收器

Java 从源代码到运行步骤

对于 Java 虚拟机来说,其实际输入的是字节码文件,而不是 Java 文件。

Java 程序从源代码到运行一般有下面 3 步:

在 JDK 的安装目录里有一个 javac 工具,就是它将 Java 代码翻译成字节码,这个工具叫做编译器。相对于后面要讲的其他编译器,其因为处于编译的前期,因此又被成为 前端编译器

image.png

当源代码转化为字节码之后,其实要运行程序,有两种选择。

  • 一种是使用 Java 解释器解释执行字节码(上图的翻译字节码),
  • 另一种则是使用 JIT 编译器将字节码转化为本地机器代码。

.class => 机器码 这一步。JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,将所有字节码都转化为机器码,而 JIT 属于运行时编译,所以运行时会有点慢。但是当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次就可以直接使用了。

所以这两种方式的区别在于,前者启动速度快但运行速度慢,而后者启动速度慢但运行速度快

机器码的运行效率肯定是高于 Java 解释器的。所以在实际情况中,为了运行速度以及效率,我们通常采用两者相结合的方式进行 Java 代码的编译执行。

HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。

JVM 中一个 class 文件的一生

JVM 的架构模型

Java 编译器输入的指令流基本上是一种 基于栈的指令集架构,另外一种指令集架构则是 基于寄存器的指令集架构

基于栈式架构的特点

  • 设计和实现更简单,适用于资源受限的系统;
  • 避开了寄存器的分配难题:使用零地址指令方式分配。(就是入栈执行完就出栈,所以无需地址)
  • 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
  • 不需要硬件支持(栈是内存层面的),可移植性更好,更好实现跨平台

CPU的运算速度是非常快的,为了性能 CPU 在内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。我们称这一小块临时存储区域为寄存器。

基于寄存器架构的特点

  • 典型的应用是 x86 的二进制指令集:比如传统的 PC 以及 Android 的 Davlik 虚拟机(安卓开发的 Java就是执行在这个虚拟机上的)。
  • 指令集架构则完全依赖硬件,可移植性差
  • 性能优秀和执行更高效
  • 花费更少的指令去完成一项操作。(基于栈式的架构还需要出栈和入栈)
  • 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主(区别于上面的使用栈进行操作,这个指令执行则是采用 地址 + 操作数 这样的形式)

编译下面这个 Java 文件

public class Temp {
public static void main(String[] args) {
int i = 99;
int j = 101;
int k = i + j;
}
}

检查生成 class 字节码文件

# 使用 javap 检查编译后面的文件
javap -v Temp.class

可以看到输出的文件

Code:
stack=2, locals=4, args_size=1
0: bipush 99 // 常量 99入栈
2: istore_1 // 分配到索引 1处
3: bipush 101 // 常量 101入栈
5: istore_2 // 分配到索引 2处
6: iload_1
7: iload_2
8: iadd // 常量99、101出栈,执行相加
9: istore_3 // 结果200入栈
10: return

而基于寄存器的计算流程

mov eax,2 // 将 eax寄存器的值设为1
mov eax,3 // 使 eax寄存器的值加3

除了使用 javap 命令,还可以在 idea 安装一个叫做 jclasslib bytecode viewer 的插件

image.png

image.png

JVM 的生命周期

虚拟机的启动

Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。

虚拟机的执行

  • 一个运行中的 Java 虚拟机有着一个清晰的任务:执行 Java 程序。
  • 程序开始执行时他才运行,程序结束时他就停止。
  • 执行一个所谓的 Java 程序的时候,真真正正在执行的是一个叫做 Java 虚拟机的进程
# 使用 jps 可以查看当前的 java 进程
> jps
10048 Jps
10424 Launcher
11832

虚拟机的退出

有如下的几种情况:

  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统用现错误而导致Java虚拟机进程终止
  • 某线程调用 Runtime 类或 System 类的 exit 方法,或 Runtime 类的 halt 方法,并且 Java 安全管理器也允许这次 exit 或 halt 操作。
  • 除此之外,JNI(Java Native Interface)规范描述了用 JNI Invocation API 来加载或卸载 Java 虚拟机时,Java虚拟机的退出情况。

运行时数据区

参考资料 【深入理解JVM】JVM的内存结构(堆、栈、GC)

由 堆、栈、程序计数器、本地方法栈、方法区 五部分组成。

其中:Java栈、本地方法栈、程序计数器 是每个线程独有一份的(紫色区域)。方法区和堆则是所有线程共有的(绿色区域)

上面的 Java栈、本地方法栈、程序计数器 统称为虚拟机栈

具体细节看各篇对应的笔记